# -*- coding: utf-8 -*-
# Gestión de visualización y ejecución de código Python.

## vipera, un diseñador de aplicaciones para Python
##     Copyright (C) 2011 Ángel Luis García García
##
##    This program is free software: you can redistribute it and/or modify
##    it under the terms of the GNU General Public License as published by
##    the Free Software Foundation, either version 3 of the License, or
##    (at your option) any later version.
##
##    This program is distributed in the hope that it will be useful,
##    but WITHOUT ANY WARRANTY; without even the implied warranty of
##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##    GNU General Public License for more details.
##
##    You should have received a copy of the GNU General Public License
##    along with this program.  If not, see <http://www.gnu.org/licenses/>.

import wx
import wx.stc as stc

import os
import keyword
import codecs
import sys
import subprocess, shlex
import threading

# from lanzadera import lanzadera
from editor_vista import panel_codigo
from i18n_vipera import t

def cadenaWindows(cadena):
    '''Parseo de rutas en plataformas Microsoft'''
    # Si estamos en Windows y hay espacios en blanco en la cadena
    # hay que incluir comillas dobles alrededor de la cadena.
    if os.name == 'nt' and cadena.count(" ") > 0:
        cadena = "%s%s%s" % ('"', cadena, '"')
    # Devolvemos cadena.
    return cadena

if os.name == 'posix':
    faces = { 'times': 'Times',
              'mono' : 'Courier',
              'helv' : 'Helvetica',
              'other': 'new century schoolbook',
              'size' : 10,
              'size2': 8,
             }

if os.name == 'nt':
    faces = { 'times': 'Times New Roman',
              'mono' : 'Courier New',
              'helv' : 'Arial',
              'other': 'Comic Sans MS',
              'size' : 10,
              'size2': 8,
             }    

class EditorSTC(stc.StyledTextCtrl):
    '''Editor de presentación de código Python'''
    fold_symbols = 2

    def __init__(self, parent):
        '''Constructor de clase'''
        stc.StyledTextCtrl.__init__(self, parent, -1)

        self.CmdKeyAssign(ord('B'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
        self.CmdKeyAssign(ord('N'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)

        self.SetLexer(stc.STC_LEX_PYTHON)
        self.SetKeyWords(0, " ".join(keyword.kwlist))
        self.SetProperty("fold", "1")
        self.SetProperty("tab.timmy.whinge.level", "1")
        self.SetMargins(0,0)

        self.SetViewWhiteSpace(False)
        self.SetEdgeMode(stc.STC_EDGE_BACKGROUND)
        self.SetEdgeColumn(78)

        # Setup a margin to hold fold markers
        self.SetMarginType(2, stc.STC_MARGIN_SYMBOL)
        self.SetMarginMask(2, stc.STC_MASK_FOLDERS)
        self.SetMarginSensitive(2, True)
        self.SetMarginWidth(2, 12)

        if self.fold_symbols == 0:
            # Arrow pointing right for contracted folders, arrow pointing down for expanded
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN,    stc.STC_MARK_ARROWDOWN, "black", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDER,        stc.STC_MARK_ARROW, "black", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB,     stc.STC_MARK_EMPTY, "black", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL,    stc.STC_MARK_EMPTY, "black", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND,     stc.STC_MARK_EMPTY, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_EMPTY, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_EMPTY, "white", "black")
            
        elif self.fold_symbols == 1:
            # Plus for contracted folders, minus for expanded
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN,    stc.STC_MARK_MINUS, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDER,        stc.STC_MARK_PLUS,  "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB,     stc.STC_MARK_EMPTY, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL,    stc.STC_MARK_EMPTY, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND,     stc.STC_MARK_EMPTY, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_EMPTY, "white", "black")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_EMPTY, "white", "black")

        elif self.fold_symbols == 2:
            # Like a flattened tree control using circular headers and curved joins
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN,    stc.STC_MARK_CIRCLEMINUS,          "white", "#404040")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDER,        stc.STC_MARK_CIRCLEPLUS,           "white", "#404040")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB,     stc.STC_MARK_VLINE,                "white", "#404040")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL,    stc.STC_MARK_LCORNERCURVE,         "white", "#404040")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND,     stc.STC_MARK_CIRCLEPLUSCONNECTED,  "white", "#404040")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_CIRCLEMINUSCONNECTED, "white", "#404040")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNERCURVE,         "white", "#404040")

        elif self.fold_symbols == 3:
            # Like a flattened tree control using square headers
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN,    stc.STC_MARK_BOXMINUS,          "white", "#808080")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDER,        stc.STC_MARK_BOXPLUS,           "white", "#808080")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB,     stc.STC_MARK_VLINE,             "white", "#808080")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL,    stc.STC_MARK_LCORNER,           "white", "#808080")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEREND,     stc.STC_MARK_BOXPLUSCONNECTED,  "white", "#808080")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "#808080")
            self.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER,           "white", "#808080")


        self.Bind(stc.EVT_STC_MARGINCLICK, self.OnMarginClick)
        
        # Global default styles for all languages
        self.StyleSetSpec(stc.STC_STYLE_DEFAULT,     "face:%(helv)s,size:%(size)d" % faces)
        self.StyleClearAll()  # Reset all to be like the default

        # Global default styles for all languages
        self.StyleSetSpec(stc.STC_STYLE_DEFAULT,     "face:%(helv)s,size:%(size)d" % faces)
        self.StyleSetSpec(stc.STC_STYLE_LINENUMBER,  "back:#C0C0C0,face:%(helv)s,size:%(size2)d" % faces)
        self.StyleSetSpec(stc.STC_STYLE_CONTROLCHAR, "face:%(other)s" % faces)
        self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,  "fore:#FFFFFF,back:#0000FF,bold")
        self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,    "fore:#000000,back:#FF0000,bold")

        # Python styles
        # Default
        self.StyleSetSpec(stc.STC_P_DEFAULT, "fore:#000000,face:%(helv)s,size:%(size)d" % faces)
        # Comments
        self.StyleSetSpec(stc.STC_P_COMMENTLINE, "fore:#007F00,face:%(other)s,size:%(size)d" % faces)
        # Number
        self.StyleSetSpec(stc.STC_P_NUMBER, "fore:#007F7F,size:%(size)d" % faces)
        # String
        self.StyleSetSpec(stc.STC_P_STRING, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces)
        # Single quoted string
        self.StyleSetSpec(stc.STC_P_CHARACTER, "fore:#7F007F,face:%(helv)s,size:%(size)d" % faces)
        # Keyword
        self.StyleSetSpec(stc.STC_P_WORD, "fore:#00007F,bold,size:%(size)d" % faces)
        # Triple quotes
        self.StyleSetSpec(stc.STC_P_TRIPLE, "fore:#7F0000,size:%(size)d" % faces)
        # Triple double quotes
        self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, "fore:#7F0000,size:%(size)d" % faces)
        # Class name definition
        self.StyleSetSpec(stc.STC_P_CLASSNAME, "fore:#0000FF,bold,underline,size:%(size)d" % faces)
        # Function or method name definition
        self.StyleSetSpec(stc.STC_P_DEFNAME, "fore:#007F7F,bold,size:%(size)d" % faces)
        # Operators
        self.StyleSetSpec(stc.STC_P_OPERATOR, "bold,size:%(size)d" % faces)
        # Identifiers
        self.StyleSetSpec(stc.STC_P_IDENTIFIER, "fore:#000000,face:%(helv)s,size:%(size)d" % faces)
        # Comment-blocks
        self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, "fore:#7F7F7F,size:%(size)d" % faces)
        # End of line where string is not closed
        self.StyleSetSpec(stc.STC_P_STRINGEOL, "fore:#000000,face:%(mono)s,back:#E0C0E0,eol,size:%(size)d" % faces)
        self.SetCaretForeground("GREEN")
        
        self.SetMarginType(1, stc.STC_MARGIN_NUMBER)
        self.SetMarginWidth(1, 25)

    def OnMarginClick(self, evt):
        '''Click en márgen izquierdo'''
        # fold and unfold as needed
        if evt.GetMargin() == 2:
            if evt.GetShift() and evt.GetControl():
                self.FoldAll()
            else:
                lineClicked = self.LineFromPosition(evt.GetPosition())

                if self.GetFoldLevel(lineClicked) & stc.STC_FOLDLEVELHEADERFLAG:
                    if evt.GetShift():
                        self.SetFoldExpanded(lineClicked, True)
                        self.Expand(lineClicked, True, True, 1)
                    elif evt.GetControl():
                        if self.GetFoldExpanded(lineClicked):
                            self.SetFoldExpanded(lineClicked, False)
                            self.Expand(lineClicked, False, True, 0)
                        else:
                            self.SetFoldExpanded(lineClicked, True)
                            self.Expand(lineClicked, True, True, 100)
                    else:
                        self.ToggleFold(lineClicked)

    def FoldAll(self):
        '''FoldAll'''
        lineCount = self.GetLineCount()
        expanding = True

        # find out if we are folding or unfolding
        for lineNum in range(lineCount):
            if self.GetFoldLevel(lineNum) & stc.STC_FOLDLEVELHEADERFLAG:
                expanding = not self.GetFoldExpanded(lineNum)
                break

        lineNum = 0

        while lineNum < lineCount:
            level = self.GetFoldLevel(lineNum)
            if level & stc.STC_FOLDLEVELHEADERFLAG and \
               (level & stc.STC_FOLDLEVELNUMBERMASK) == stc.STC_FOLDLEVELBASE:

                if expanding:
                    self.SetFoldExpanded(lineNum, True)
                    lineNum = self.Expand(lineNum, True)
                    lineNum = lineNum - 1
                else:
                    lastChild = self.GetLastChild(lineNum, -1)
                    self.SetFoldExpanded(lineNum, False)

                    if lastChild > lineNum:
                        self.HideLines(lineNum+1, lastChild)

            lineNum = lineNum + 1

    def Expand(self, line, doExpand, force=False, visLevels=0, level=-1):
        '''Expand'''
        lastChild = self.GetLastChild(line, level)
        line = line + 1

        while line <= lastChild:
            if force:
                if visLevels > 0:
                    self.ShowLines(line, line)
                else:
                    self.HideLines(line, line)
            else:
                if doExpand:
                    self.ShowLines(line, line)

            if level == -1:
                level = self.GetFoldLevel(line)

            if level & stc.STC_FOLDLEVELHEADERFLAG:
                if force:
                    if visLevels > 1:
                        self.SetFoldExpanded(line, True)
                    else:
                        self.SetFoldExpanded(line, False)

                    line = self.Expand(line, doExpand, force, visLevels-1)

                else:
                    if doExpand and self.GetFoldExpanded(line):
                        line = self.Expand(line, True, force, visLevels-1)
                    else:
                        line = self.Expand(line, False, force, visLevels-1)
            else:
                line = line + 1

        return line
        
class panel_codigo_p(panel_codigo):
    '''Panel de visualización de código'''
    def __init__(self, parent, f_cod, f_sal, f_err, interprete, lanzadera, idioma = None):
        '''Constructor de clase'''
        # Constructor padre.
        panel_codigo.__init__(self, parent)
        # Idioma.
        self.l = idioma
        # Cambiamos el idioma a los widgets generados con wxFB.
        self.m_bpButton_codigo.SetToolTipString(t(u'Ejecutar código Python', self.l))
        self.m_bpButton_stop_eje.SetToolTipString(t(u'Parar script en ejecución', self.l))
        # Editor STC.
        self.__editor = EditorSTC(self.m_panel_codigo)
        # Sizer.
        sizer = wx.BoxSizer(wx.VERTICAL)
        # Incluimos editor en el sizer.
        sizer.Add(self.__editor, 1, wx.ALL|wx.EXPAND, 10)
        # Añadimos el sizer al panel.
        self.m_panel_codigo.SetSizer(sizer)
        # El editor STC será de solo lecura.
        self.__editor.SetReadOnly(True)
        # Demás atributos.
        self.__f_cod = f_cod
        self.__f_sal = f_sal
        self.__f_err = f_err
        self.__lanzadera = lanzadera
        self.__interprete = interprete
    
    def get_editor(self):
        '''Devolvemos la referencia al editor'''
        return self.__editor
    
    def set_codigo(self, texto, opcion = 0):
        '''Se incluye "texto" en el editor STC'''
        self.__editor.SetReadOnly(False)
        self.__editor.SetText(texto)
        self.__editor.SetReadOnly(True)
        # Si el flag está activado, se limpia 
        if opcion == 1: 
            self.m_textCtrl_errores.SetValue('')
            self.m_textCtrl_salida.SetValue('')
            
    # ###################################
    # Override de manejadores de eventos.
    # ###################################
        
    def lanzar_proceso(self, comando, lista_salida, lista_error):
        '''Método para lanzar proceso de script Python'''
        # Bandera para parar ejecución de script.
        self.parar = False

        # Formamos argumentos y creamos proceso.
        args = shlex.split(comando)
        self.proceso = subprocess.Popen(comando, 
                                        shell = True,
                                        stdout = subprocess.PIPE,
                                        stderr = subprocess.PIPE)
        while True:
            # Salida.
            if os.name == 'posix':
                linea = self.proceso.stdout.readline()
            if os.name == 'nt':
                linea = self.proceso.stdout.readline().decode('utf-8','replace')
            if len(linea.strip()) != 0: 
                lista_salida.append(linea)
            else:
                # Errores.
                linea_err = self.proceso.stderr.readlines()
                if len(linea_err) != 0: 
                    lineas_nuevas = []
                    lineas_nuevas.append(linea_err[0])
                    lineas_nuevas.extend(linea_err[7:])
                    for i in lineas_nuevas: 
                        if os.name == 'posix':
                            lista_error.append(i) #.decode('utf-8','replace'))
                        if os.name == 'nt':
                            lista_error.append(i.decode('utf-8','replace'))                            
            # ¿El usuario terminó el proceso?
            if self.parar is True:
                break
            # ¿Terminó el proceso?
            if self.proceso.poll() is not None:
                break
         
    def OnEjecutarCodigo( self, event ):
        '''Event Handler para ejecución de código Python del editor'''
        # Limpiamos...
        self.m_textCtrl_errores.SetValue("")
        self.m_textCtrl_salida.SetValue("")
        # Texto del editor.
        texto_codigo = self.__editor.GetText()
        # Si no hay nada en el editor, nos vamos.
        if len(texto_codigo.strip()) == 0: 
            wx.MessageBox(t(u"Seleccione un módulo", self.l), t(u"Atención", self.l), wx.OK)
            return 
        # Fichero de código.
        f = codecs.open(self.__f_cod,'w','utf-8')
        f.write(texto_codigo)
        f.close()
        # Limpiamos ficheros de salida y errores.
        codecs.open(self.__f_err,'w','utf-8').close()
        codecs.open(self.__f_sal,'w','utf-8').close()
        # Configuramos parámetros para lanzamiento.
        codigo = "%s %s %s %s %s" % (cadenaWindows(os.path.realpath(self.__lanzadera)),
                                  cadenaWindows(os.path.realpath(self.__f_cod)),
                                  cadenaWindows(os.path.realpath(self.__f_sal)),
                                  cadenaWindows(os.path.realpath(self.__f_err)),
                                  0)
                
        # Formamos el comando completo.
        comando = "%s %s" % (self.__interprete, codigo)
        
        # Desactivamos botones.
        self.m_bpButton_codigo.Enable(False)
        
        # Creamos lista de salida y error.
        lista_salida = list()
        lista_error = list()
        # Creamos hilo de ejecución.
        self.thread = threading.Thread(target=self.lanzar_proceso, args=(comando,
                                                                         lista_salida,
                                                                         lista_error))
        self.thread.setDaemon(True) # True
        numero_hilos_inicial = len(threading.enumerate())
        self.thread.start()
        
        while True:
            if len(threading.enumerate()) > numero_hilos_inicial:
                wx.Yield()
                continue
            else:
                # Comprobamos si el widget contenedor (panel) sigue existiendo.
                if not self: break
                # Comprobamos si el widget sigue existiendo.
                if self.m_textCtrl_salida:
                    # Añadimos texto.
                    for linea in lista_salida: self.m_textCtrl_salida.AppendText(linea)
                # Comprobamos si el widget sigue existiendo.                    
                if self.m_textCtrl_errores:
                    # Añadimos texto.
                    for linea in lista_error: self.m_textCtrl_errores.AppendText(linea)
                # Comprobamos si el widget sigue existiendo.                    
                if self.m_bpButton_codigo:
                    # Añadimos texto.
                    self.m_bpButton_codigo.Enable(True)
                break
        
    def matar_proceso(self):
        '''Método de eliminación de procesos multiplataforma POSIX/Microsoft'''
        self.parar = True
        try:
            parada = False
            # Matamos el proceso en plataformas POSIX.
            if os.name == 'posix' and self.proceso.poll() is None:       
                cadena = "kill -HUP `ps -A -ostat,pid,ppid,cmd | grep 'lanzadera.py' | awk '{print $2}'`"
                os.system(cadena) 
                parada = True
                # Matamos el proceso en plataformas Microsoft Windows 7, ¿XP?
            elif os.name == 'nt' and self.proceso.poll() is None:       
                cadena = "taskkill /PID %s /T /F" % self.proceso.pid
                os.system(cadena)
                parada = True
            if parada:
                msj = t(u'Se paró el script Python', self.l)
            else:
                msj = t(u"No hay ningún script en proceso", self.l)
            # Devolvemos mensaje.
            return msj
        except:
            return t(u'No se ha podido matar el proceso', self.l)
        
    def OnStopEjecucion(self, event):
        '''Parar la ejecución de un proceso'''
        # Activamos botones.
        self.m_bpButton_codigo.Enable(True)
        # Matamos el proceso.
        msj = self.matar_proceso()
        # Mostramos información
        wx.MessageBox(msj,t(u"Atención", self.l),wx.OK)
        